#pragma once

#define GLFW_NO_GLU
#define GLFW_INCLUDE_GL3
#include <GL/glfw3.h>
#include <GL3/gl3w.h>

#include <string>
#include <iostream>
#include <memory>
#include <functional>

class GLContext
{
public:
	GLContext(int width, int height, bool fullscreen, const std::string &title) 
		: width(width), height(height)
	{
		if (!glfwInit()) throw std::runtime_error("Failed to initialize GLFW: " + std::string(glfwErrorString(glfwGetError())));

		wnd = glfwOpenWindow(width, height, fullscreen ? GLFW_FULLSCREEN : GLFW_WINDOWED, title.c_str(), nullptr);
        if (!wnd) throw std::runtime_error("Failed to open GLFW window: " + std::string(glfwErrorString(glfwGetError())));

		glfwMakeContextCurrent(wnd);

		glfwSetWindowCloseCallback(GLContext::closeWindow);
		glfwSetWindowUserPointer(wnd, this);
		glfwSetKeyCallback(GLContext::keyCallback);
		//glfwSetCharCallback(GLContext::charCallback);
		glfwSetMouseButtonCallback(GLContext::mouseButtonCallback);
		glfwSetMousePosCallback(GLContext::mousePositionCallback);

		//////////////////////////////////////////
		// GL3W INITIALIZING
		//////////////////////////////////////////
		GLenum gl3wInitErr = gl3wInit();
		if(gl3wInitErr)
			throw std::runtime_error("Failed to initialize OpenGL!");
		if(gl3wIsSupported(3, 3) == false)
			throw std::runtime_error("Opengl 3.3 is not supported!");
		wgl3wSwapIntervalEXT(fullscreen ? 1 : 0);

	}

	~GLContext()
	{
		glfwTerminate();
	}

	void closeWindow()
	{
		glfwCloseWindow(wnd);
	}

	static int closeWindow(GLFWwindow wnd)
	{
		GLContext *ptr = static_cast<GLContext*>(glfwGetWindowUserPointer(wnd));
		if(!ptr->window_close_callback._Empty())
			ptr->window_close_callback();
		return 1;
	}

	void setWindowCloseCallback(std::function<void()> callback)
	{
		window_close_callback = callback;
	}

	void swapBuffers()
	{
		glfwSwapBuffers();
	}

	void pollEvents()
	{
		glfwPollEvents();
	}

	bool windowIsOpen()
	{
		return glfwIsWindow(wnd) ? true : false;
	}

	double getTime()
	{
		return glfwGetTime();
	}

	void setTime(double time)
	{
		glfwSetTime(time);
	}

	void setKeyCallback(std::function<void(int, int)> callback)
	{
		key_callback = callback;
	}
	void setMouseButtonCallback(std::function<void(int, int)> callback)
	{
		mouse_button_callback = callback;
	}
	void setMousePositionCallback(std::function<void(int, int)> callback)
	{
		mouse_position_callback = callback;
	}

	private:
		static void keyCallback(GLFWwindow wnd, int key, int down)
		{
			GLContext *ptr = static_cast<GLContext*>(glfwGetWindowUserPointer(wnd));
			if(!ptr->key_callback._Empty())
				ptr->key_callback(key, down);
		}
		static void charCallback(GLFWwindow wnd, int key)
		{
			GLContext *ptr = static_cast<GLContext*>(glfwGetWindowUserPointer(wnd));
			if(!ptr->key_callback._Empty())
				ptr->key_callback(key, true);
		}
		static void mouseButtonCallback(GLFWwindow wnd, int button, int pressed)
		{
			GLContext *ptr = static_cast<GLContext*>(glfwGetWindowUserPointer(wnd));
			if(!ptr->mouse_button_callback._Empty())
				ptr->mouse_button_callback(button, pressed);
		}
		static void mousePositionCallback(GLFWwindow wnd, int x, int y)
		{
			GLContext *ptr = static_cast<GLContext*>(glfwGetWindowUserPointer(wnd));
			if(!ptr->mouse_position_callback._Empty())
				ptr->mouse_position_callback(x, y);
		}

		int width, height;
		GLFWwindow wnd;
		std::function<void()> window_close_callback;
		std::function<void(int, int)> key_callback;
		std::function<void(int, int)> mouse_button_callback;
		std::function<void(int, int)> mouse_position_callback;
};
